/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.config.spring.beans.factory.annotation;

import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.config.MethodConfig;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.config.spring.ServiceBean;
import org.apache.dubbo.config.spring.context.annotation.DubboClassPathBeanDefinitionScanner;

import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static org.apache.dubbo.config.spring.util.ObjectUtils.of;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
import static org.springframework.context.annotation.AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.util.ClassUtils.resolveClassName;

/**
 * {@link Service} Annotation {@link BeanDefinitionRegistryPostProcessor Bean Definition Registry
 * Post Processor}
 *
 * @since 2.5.8
 */
public class ServiceAnnotationBeanPostProcessor implements BeanDefinitionRegistryPostProcessor,
    EnvironmentAware,
    ResourceLoaderAware, BeanClassLoaderAware {


  private final Logger logger = LoggerFactory.getLogger(getClass());

  private final Set<String> packagesToScan;

  private Environment environment;

  private ResourceLoader resourceLoader;

  private ClassLoader classLoader;

  public ServiceAnnotationBeanPostProcessor(String... packagesToScan) {
    this(Arrays.asList(packagesToScan));
  }

  public ServiceAnnotationBeanPostProcessor(Collection<String> packagesToScan) {
    this(new LinkedHashSet<String>(packagesToScan));
  }

  public ServiceAnnotationBeanPostProcessor(Set<String> packagesToScan) {
    this.packagesToScan = packagesToScan;
  }

  /**
   * 该方法的主要作用在于，在基础的BeanDefinition注册完毕之后，调用该方法。
   * 本类中主要目的是扫描dubbo的bean，并将其注册到BeanDefinitionRegistry中，
   * 并且将dubbo的ServiceBean注册到BeanDefinitionRegistry中
   */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
      throws BeansException {

    // packagesToScan中保存了指定的要扫描的dubbo类路径，这里主要是对这些路径中的占位符进行处理，
    // 以将其替换为相应的值
    Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

    if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
      // 这里主要是进行了两个动作，一个是扫描当前类路径中所有标注了@org.apache.dubbo.config.annotationService
      // 注解的类，将其当做一个bean进行注册；另一个是注册一个目标类为ServiceBean的BeanDefinition。这个ServiceBean
      // 是dubbo实现服务注册的关键，它进行了诸如服务注册，连接建立等工作
      registerServiceBeans(resolvedPackagesToScan, registry);
    } else {
      if (logger.isWarnEnabled()) {
        logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
      }
    }

  }


  /**
   * Registers Beans whose classes was annotated {@link Service}
   *
   * @param packagesToScan The base packages to scan
   * @param registry {@link BeanDefinitionRegistry}
   */
  private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

    // 这个类的主要作用是创建一个BeanDefinitionScanner，该scanner的作用是扫描classpath中的所有class文件，
    // 并且判断该文件是否使用了dubbo的@Service注解进行标注，如果标注了则将其作为一个bean注册到BeanDefinitionRegistry中
    DubboClassPathBeanDefinitionScanner scanner =
        new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

    // 获取一个beanName生成器，其逻辑是首先检查当前BeanDefinitionRegistry中是否存在名称为CONFIGURATION_BEAN_NAME_GENERATOR
    // 指定的名称的bean，如果存在，则使用该bean来为各个bean生成名称，如果不存在，则创建一个AnnotationBeanNameGenerator返回
    BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

    scanner.setBeanNameGenerator(beanNameGenerator);

    // 添加一个Filter，表示使用了该注解的bean是我们需要处理的一种bean，
    // 这里相当于在遍历指定classpath下指定目录里的class文件时，只获取标注了@org.apache.dubbo.config.annotation.Service的类
    scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));

    for (String packageToScan : packagesToScan) {

      // 查找目标package中所有使用了@Service注解进行标注的类，并且将其封装为一个BeanDefinition注册到BeanDefinitionRegistry中
      scanner.scan(packageToScan);

      // 查找所有使用@Service标注的类，获取其使用BeanDefinitionHolder封装后的类
      Set<BeanDefinitionHolder> beanDefinitionHolders =
          findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

      if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
          // 将每一个使用@Service标注的类都封装到一个ServiceBean中，以便生成一个dubbo的接口
          registerServiceBean(beanDefinitionHolder, registry, scanner);
        }

        if (logger.isInfoEnabled()) {
          logger.info(beanDefinitionHolders.size() + " annotated Dubbo's @Service Components { " +
              beanDefinitionHolders +
              " } were scanned under package[" + packageToScan + "]");
        }

      } else {

        if (logger.isWarnEnabled()) {
          logger.warn("No Spring Bean annotating Dubbo's @Service was found under package["
              + packageToScan + "]");
        }

      }

    }

  }

  /**
   * It'd better to use BeanNameGenerator instance that should reference {@link
   * ConfigurationClassPostProcessor#componentScanBeanNameGenerator}, thus it maybe a potential
   * problem on bean name generation.
   *
   * @param registry {@link BeanDefinitionRegistry}
   * @return {@link BeanNameGenerator} instance
   * @see SingletonBeanRegistry
   * @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
   * @see ConfigurationClassPostProcessor#processConfigBeanDefinitions
   * @since 2.5.8
   */
  private BeanNameGenerator resolveBeanNameGenerator(BeanDefinitionRegistry registry) {

    BeanNameGenerator beanNameGenerator = null;

    if (registry instanceof SingletonBeanRegistry) {
      SingletonBeanRegistry singletonBeanRegistry = SingletonBeanRegistry.class.cast(registry);
      beanNameGenerator = (BeanNameGenerator) singletonBeanRegistry
          .getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
    }

    if (beanNameGenerator == null) {

      if (logger.isInfoEnabled()) {

        logger.info("BeanNameGenerator bean can't be found in BeanFactory with name ["
            + CONFIGURATION_BEAN_NAME_GENERATOR + "]");
        logger.info("BeanNameGenerator will be a instance of " +
            AnnotationBeanNameGenerator.class.getName() +
            " , it maybe a potential problem on bean name generation.");
      }

      beanNameGenerator = new AnnotationBeanNameGenerator();

    }

    return beanNameGenerator;

  }

  /**
   * Finds a {@link Set} of {@link BeanDefinitionHolder BeanDefinitionHolders} whose bean type
   * annotated {@link Service} Annotation.
   *
   * @param scanner {@link ClassPathBeanDefinitionScanner}
   * @param packageToScan pachage to scan
   * @param registry {@link BeanDefinitionRegistry}
   * @return non-null
   * @since 2.5.8
   */
  private Set<BeanDefinitionHolder> findServiceBeanDefinitionHolders(
      ClassPathBeanDefinitionScanner scanner, String packageToScan, BeanDefinitionRegistry registry,
      BeanNameGenerator beanNameGenerator) {

    Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(packageToScan);

    Set<BeanDefinitionHolder> beanDefinitionHolders = new LinkedHashSet<BeanDefinitionHolder>(
        beanDefinitions.size());

    for (BeanDefinition beanDefinition : beanDefinitions) {

      String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
      BeanDefinitionHolder beanDefinitionHolder = new BeanDefinitionHolder(beanDefinition,
          beanName);
      beanDefinitionHolders.add(beanDefinitionHolder);

    }

    return beanDefinitionHolders;

  }

  /**
   * Registers {@link ServiceBean} from new annotated {@link Service} {@link BeanDefinition}
   *
   * @see ServiceBean
   * @see BeanDefinition
   */
  private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder,
      BeanDefinitionRegistry registry,
      DubboClassPathBeanDefinitionScanner scanner) {

    Class<?> beanClass = resolveClass(beanDefinitionHolder);

    Service service = findAnnotation(beanClass, Service.class);

    Class<?> interfaceClass = resolveServiceInterfaceClass(beanClass, service);

    String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

    AbstractBeanDefinition serviceBeanDefinition =
        buildServiceBeanDefinition(service, interfaceClass, annotatedServiceBeanName);

    // ServiceBean Bean name
    String beanName = generateServiceBeanName(service, interfaceClass, annotatedServiceBeanName);

    if (scanner
        .checkCandidate(beanName, serviceBeanDefinition)) { // check duplicated candidate bean
      registry.registerBeanDefinition(beanName, serviceBeanDefinition);

      if (logger.isInfoEnabled()) {
        logger.info("The BeanDefinition[" + serviceBeanDefinition +
            "] of ServiceBean has been registered with name : " + beanName);
      }

    } else {

      if (logger.isWarnEnabled()) {
        logger.warn("The Duplicated BeanDefinition[" + serviceBeanDefinition +
            "] of ServiceBean[ bean name : " + beanName +
            "] was be found , Did @DubboComponentScan scan to same package in many times?");
      }

    }

  }

  /**
   * Generates the bean name of {@link ServiceBean}
   *
   * @param interfaceClass the class of interface annotated {@link Service}
   * @param annotatedServiceBeanName the bean name of annotated {@link Service}
   * @return ServiceBean@interfaceClassName#annotatedServiceBeanName
   * @since 2.5.9
   */
  private String generateServiceBeanName(Service service, Class<?> interfaceClass,
      String annotatedServiceBeanName) {

    ServiceBeanNameBuilder builder = ServiceBeanNameBuilder
        .create(service, interfaceClass, environment);

    return builder.build();

  }

  private Class<?> resolveServiceInterfaceClass(Class<?> annotatedServiceBeanClass,
      Service service) {

    Class<?> interfaceClass = service.interfaceClass();

    if (void.class.equals(interfaceClass)) {

      interfaceClass = null;

      String interfaceClassName = service.interfaceName();

      if (StringUtils.hasText(interfaceClassName)) {
        if (ClassUtils.isPresent(interfaceClassName, classLoader)) {
          interfaceClass = resolveClassName(interfaceClassName, classLoader);
        }
      }

    }

    if (interfaceClass == null) {

      Class<?>[] allInterfaces = annotatedServiceBeanClass.getInterfaces();

      if (allInterfaces.length > 0) {
        interfaceClass = allInterfaces[0];
      }

    }

    Assert.notNull(interfaceClass,
        "@Service interfaceClass() or interfaceName() or interface class must be present!");

    Assert.isTrue(interfaceClass.isInterface(),
        "The type that was annotated @Service is not an interface!");

    return interfaceClass;
  }

  private Class<?> resolveClass(BeanDefinitionHolder beanDefinitionHolder) {

    BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();

    return resolveClass(beanDefinition);

  }

  private Class<?> resolveClass(BeanDefinition beanDefinition) {

    String beanClassName = beanDefinition.getBeanClassName();

    return resolveClassName(beanClassName, classLoader);

  }

  private Set<String> resolvePackagesToScan(Set<String> packagesToScan) {
    Set<String> resolvedPackagesToScan = new LinkedHashSet<String>(packagesToScan.size());
    for (String packageToScan : packagesToScan) {
      if (StringUtils.hasText(packageToScan)) {
        String resolvedPackageToScan = environment.resolvePlaceholders(packageToScan.trim());
        resolvedPackagesToScan.add(resolvedPackageToScan);
      }
    }
    return resolvedPackagesToScan;
  }

  private AbstractBeanDefinition buildServiceBeanDefinition(Service service,
      Class<?> interfaceClass,
      String annotatedServiceBeanName) {

    BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBean.class);

    AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

    MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();

    String[] ignoreAttributeNames = of("provider", "monitor", "application", "module", "registry",
        "protocol",
        "interface", "interfaceName");

    propertyValues.addPropertyValues(
        new AnnotationPropertyValuesAdapter(service, environment, ignoreAttributeNames));

    // References "ref" property to annotated-@Service Bean
    addPropertyReference(builder, "ref", annotatedServiceBeanName);
    // Set interface
    builder.addPropertyValue("interface", interfaceClass.getName());

    /**
     * Add {@link org.apache.dubbo.config.ProviderConfig} Bean reference
     */
    String providerConfigBeanName = service.provider();
    if (StringUtils.hasText(providerConfigBeanName)) {
      addPropertyReference(builder, "provider", providerConfigBeanName);
    }

    /**
     * Add {@link org.apache.dubbo.config.MonitorConfig} Bean reference
     */
    String monitorConfigBeanName = service.monitor();
    if (StringUtils.hasText(monitorConfigBeanName)) {
      addPropertyReference(builder, "monitor", monitorConfigBeanName);
    }

    /**
     * Add {@link org.apache.dubbo.config.ApplicationConfig} Bean reference
     */
    String applicationConfigBeanName = service.application();
    if (StringUtils.hasText(applicationConfigBeanName)) {
      addPropertyReference(builder, "application", applicationConfigBeanName);
    }

    /**
     * Add {@link org.apache.dubbo.config.ModuleConfig} Bean reference
     */
    String moduleConfigBeanName = service.module();
    if (StringUtils.hasText(moduleConfigBeanName)) {
      addPropertyReference(builder, "module", moduleConfigBeanName);
    }

    /**
     * Add {@link org.apache.dubbo.config.RegistryConfig} Bean reference
     */
    String[] registryConfigBeanNames = service.registry();

    List<RuntimeBeanReference> registryRuntimeBeanReferences = toRuntimeBeanReferences(
        registryConfigBeanNames);

    if (!registryRuntimeBeanReferences.isEmpty()) {
      builder.addPropertyValue("registries", registryRuntimeBeanReferences);
    }

    /**
     * Add {@link org.apache.dubbo.config.ProtocolConfig} Bean reference
     */
    String[] protocolConfigBeanNames = service.protocol();

    List<RuntimeBeanReference> protocolRuntimeBeanReferences = toRuntimeBeanReferences(
        protocolConfigBeanNames);

    if (!protocolRuntimeBeanReferences.isEmpty()) {
      builder.addPropertyValue("protocols", protocolRuntimeBeanReferences);
    }

    List<MethodConfig> methodConfigs = MethodConfig.constructMethodConfig(service.methods());
    if (!methodConfigs.isEmpty()) {
      builder.addPropertyValue("methods", methodConfigs);
    }

    return builder.getBeanDefinition();

  }


  private ManagedList<RuntimeBeanReference> toRuntimeBeanReferences(String... beanNames) {

    ManagedList<RuntimeBeanReference> runtimeBeanReferences = new ManagedList<RuntimeBeanReference>();

    if (!ObjectUtils.isEmpty(beanNames)) {

      for (String beanName : beanNames) {

        String resolvedBeanName = environment.resolvePlaceholders(beanName);

        runtimeBeanReferences.add(new RuntimeBeanReference(resolvedBeanName));
      }

    }

    return runtimeBeanReferences;

  }

  private void addPropertyReference(BeanDefinitionBuilder builder, String propertyName,
      String beanName) {
    String resolvedBeanName = environment.resolvePlaceholders(beanName);
    builder.addPropertyReference(propertyName, resolvedBeanName);
  }


  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
      throws BeansException {

  }

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = environment;
  }

  @Override
  public void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
  }

  @Override
  public void setBeanClassLoader(ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

}
